/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is Forte for Java, Community Edition. The Initial
* Developer of the Original Code is Sun Microsystems, Inc. Portions
* Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
*/
package org.netbeans.editor.ext;
import java.io.File;
import java.io.FileFilter;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.TreeSet;
import java.lang.reflect.Modifier;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
/**
* Java completion utilities
*
* @author Miloslav Metelka
* @version 1.00
*/
public class JCUtilities {
private static final boolean[][] primitivesAssignable = new boolean[][] {
new boolean[] { true, false, false, false, false, false, false, false, false}, // boolean
new boolean[] { false, true, false, true, true, true, true, true, false}, // byte
new boolean[] { false, false, true, true, true, true, true, false, false}, // char
new boolean[] { false, false, false, true, false, false, false, false, false}, // double
new boolean[] { false, false, false, true, true, false, false, false, false}, // float
new boolean[] { false, false, false, true, true, true, true, false, false}, // int
new boolean[] { false, false, false, true, true, false, true, false, false}, // long
new boolean[] { false, false, false, true, true, true, true, true, false}, // short
new boolean[] { false, false, false, false, false, false, false, false, true} // void
};
private static final JCClass[][] primitivesCommonClass = new JCClass[][] {
new JCClass[] { JCompletion.BOOLEAN_CLASS, null, null, null, null, null, null, null, null}, // boolean
new JCClass[] { null, JCompletion.BYTE_CLASS, JCompletion.INT_CLASS, JCompletion.DOUBLE_CLASS, // byte
JCompletion.FLOAT_CLASS, JCompletion.INT_CLASS, JCompletion.LONG_CLASS, JCompletion.INT_CLASS, null}, // byte
new JCClass[] { null, JCompletion.INT_CLASS, JCompletion.CHAR_CLASS, JCompletion.DOUBLE_CLASS, // char
JCompletion.FLOAT_CLASS, JCompletion.INT_CLASS, JCompletion.LONG_CLASS, JCompletion.INT_CLASS, null}, // char
new JCClass[] { null, JCompletion.DOUBLE_CLASS, JCompletion.DOUBLE_CLASS, JCompletion.DOUBLE_CLASS, // double
JCompletion.DOUBLE_CLASS, JCompletion.DOUBLE_CLASS, JCompletion.DOUBLE_CLASS, JCompletion.DOUBLE_CLASS, null}, // double
new JCClass[] { null, JCompletion.FLOAT_CLASS, JCompletion.FLOAT_CLASS, JCompletion.DOUBLE_CLASS, // float
JCompletion.FLOAT_CLASS, JCompletion.FLOAT_CLASS, JCompletion.FLOAT_CLASS, JCompletion.FLOAT_CLASS, null}, // float
new JCClass[] { null, JCompletion.INT_CLASS, JCompletion.INT_CLASS, JCompletion.DOUBLE_CLASS, // int
JCompletion.FLOAT_CLASS, JCompletion.INT_CLASS, JCompletion.LONG_CLASS, JCompletion.INT_CLASS, null}, // int
new JCClass[] { null, JCompletion.LONG_CLASS, JCompletion.LONG_CLASS, JCompletion.DOUBLE_CLASS, // long
JCompletion.FLOAT_CLASS, JCompletion.LONG_CLASS, JCompletion.LONG_CLASS, JCompletion.LONG_CLASS, null}, // long
new JCClass[] { null, JCompletion.INT_CLASS, JCompletion.INT_CLASS, JCompletion.DOUBLE_CLASS, // short
JCompletion.FLOAT_CLASS, JCompletion.INT_CLASS, JCompletion.LONG_CLASS, JCompletion.SHORT_CLASS, null}, // short
new JCClass[] { null, null, null, null, null, null, null, null, JCompletion.VOID_CLASS} // void
};
private static boolean stringEqual(String s1, String s2) {
return (s1 == null) ? (s2 == null) : s1.equals(s2);
}
private static boolean classEqual(JCClass c1, JCClass c2) {
return (c1 == null) ? (c2 == null) : c1.equals(c2);
}
private static boolean typeEqual(JCType t1, JCType t2) {
return (t1 == null) ? (t2 == null)
: classEqual(t1.getClazz(), t2.getClazz()) && (t1.getArrayDepth() == t2.getArrayDepth());
}
private static boolean parameterEqual(JCParameter p1, JCParameter p2) {
return (p1 == null) ? (p2 == null)
: typeEqual(p1.getType(), p2.getType()) && stringEqual(p1.getName(), p2.getName());
}
private static boolean constructorEqual(JCConstructor c1, JCConstructor c2) {
return (c1 == null) ? (c2 == null)
: (c1.getClazz().equals(c2.getClazz()) // mustn't be null
&& c1.getModifiers() == c2.getModifiers()
&& parameterArrayEqual(c1.getParameters(), c2.getParameters())
&& classArrayEqual(c1.getExceptions(), c2.getExceptions())
);
}
private static boolean parameterArrayEqual(JCParameter[] pa1, JCParameter[] pa2) {
if (pa1.length != pa2.length) {
return false;
}
for (int i = pa1.length - 1; i >= 0; i--) {
if (!parameterEqual(pa1[i], pa2[i])) {
return false;
}
}
return true;
}
private static boolean classArrayEqual(JCClass[] ca1, JCClass[] ca2) {
if (ca1.length != ca2.length) {
return false;
}
for (int i = ca1.length - 1; i >= 0; i--) {
if (!classEqual(ca1[i], ca2[i])) {
return false;
}
}
return true;
}
private static boolean fieldArraysEqual(JCField[] fa1, JCField[] fa2) {
if (fa1.length != fa2.length) {
return false;
}
for (int i = fa1.length - 1; i >= 0; i--) {
JCField f1 = fa1[i];
JCField f2 = fa2[i];
if (!parameterEqual(f1, f2)
|| !f1.getClazz().equals(f2.getClazz()) // mustn't be null
|| (f1.getModifiers() != f2.getModifiers())
) {
return false;
}
}
return true;
}
private static boolean constructorArrayEqual(JCConstructor[] ca1, JCConstructor[] ca2) {
if (ca1.length != ca2.length) {
return false;
}
for (int i = ca1.length - 1; i >= 0; i--) {
if (!constructorEqual(ca1[i], ca2[i])) {
return false;
}
}
return true;
}
private static boolean methodArraysEqual(JCMethod[] ma1, JCMethod[] ma2) {
if (ma1.length != ma2.length) {
return false;
}
for (int i = ma1.length - 1; i >= 0; i--) {
JCMethod m1 = ma1[i];
JCMethod m2 = ma2[i];
if (!constructorEqual(m1, m2)
|| !stringEqual(m1.getName(), m2.getName())
|| !typeEqual(m1.getReturnType(), m2.getReturnType())
) {
return false ;
}
}
return true;
}
public static boolean equal(JCClass c1, JCClass c2) {
if (c1 == null && c2 == null) { // both null
return true;
}
if (c1 == null || c2 == null) { // one of them is null, but not both
return false;
}
if (!c1.equals(c2)
|| c1.isInterface() != c2.isInterface()
|| c1.getModifiers() != c2.getModifiers()
|| !classEqual(c1.getSuperclass(), c2.getSuperclass())
) {
return false;
}
if (!fieldArraysEqual(c1.getFields(), c2.getFields())
|| !constructorArrayEqual(c1.getConstructors(), c2.getConstructors())
|| !methodArraysEqual(c1.getMethods(), c2.getMethods())
|| !classArrayEqual(c1.getInterfaces(), c2.getInterfaces())
) {
return false;
}
return true;
}
public static String dumpClass(JCClass c) {
StringBuffer sb = new StringBuffer();
sb.append(Modifier.toString(c.getModifiers()));
sb.append(c.isInterface() ? " interface " : " class "); // NOI18N
sb.append(c);
sb.append(" extends "); // NOI18N
sb.append(c.getSuperclass());
// Add implemented interfaces
JCClass[] ifcs = c.getInterfaces();
int cntM1 = ifcs.length - 1;
if (cntM1 >= 0) {
sb.append(" implements "); // NOI18N
for (int i = 0; i <= cntM1; i++) {
sb.append(ifcs[i].toString());
if (i < cntM1) {
sb.append(", "); // NOI18N
}
}
}
sb.append('\n');
String indentStr = " "; // NOI18N
// Add fields
JCField[] flds = c.getFields();
if (flds.length > 0) {
sb.append("FIELDS:\n"); // NOI18N
for (int i = 0; i < flds.length; i++) {
sb.append(indentStr);
sb.append(flds[i]);
sb.append('\n');
}
}
// Add constructors
JCConstructor[] cons = c.getConstructors();
if (cons.length > 0) {
sb.append("CONSTRUCTORS:\n"); // NOI18N
for (int i = 0; i < cons.length; i++) {
sb.append(indentStr);
sb.append(cons[i]);
sb.append('\n');
}
}
// Add methods
JCMethod[] mtds = c.getMethods();
if (mtds.length > 0) {
sb.append("METHODS:\n"); // NOI18N
for (int i = 0; i < mtds.length; i++) {
sb.append(indentStr);
sb.append(mtds[i]);
sb.append('\n');
}
}
return sb.toString();
}
public static JCClass getExactClass(JCFinder finder, String name, String pkgName) {
return finder.getExactClass((pkgName.length() != 0) ? (pkgName + "." + name) : name); // NOI18N
}
/** Filter the list of the methods (usually returned from
* Finder.findMethods()) or the list of the constructors
* by the given parameter specification.
* @param methodList list of the methods. They should have the same
* name but in fact they don't have to.
* @param parmTypes parameter types specification. If set to null, no filtering
* is performed and the same list is returned. If a particular
* @param acceptMoreParameters useful for code completion to get
* even the methods with more parameters.
*/
public static List filterMethods(List methodList, List parmTypeList,
boolean acceptMoreParameters) {
if (parmTypeList == null) {
return methodList;
}
List ret = new ArrayList();
int parmTypeCnt = parmTypeList.size();
int cnt = methodList.size();
for (int i = 0; i < cnt; i++) {
// Use constructor conversion to allow to use it too for the constructors
JCConstructor m = (JCConstructor)methodList.get(i);
JCParameter[] methodParms = m.getParameters();
if (methodParms.length == parmTypeCnt
|| (acceptMoreParameters && methodParms.length >= parmTypeCnt)
) {
boolean accept = true;
boolean bestMatch = !acceptMoreParameters;
for (int j = 0; accept && j < parmTypeCnt; j++) {
JCType mpt = methodParms[j].getType();
JCType t = (JCType)parmTypeList.get(j);
if (t != null) {
if (!t.equals(mpt)) {
bestMatch = false;
if (!isAssignable(t, mpt)) {
accept = false;
break;
}
}
} else { // type in list is null
bestMatch = false;
}
}
if (accept) {
if (bestMatch) {
ret.clear();
}
ret.add(m);
if (bestMatch) {
break;
}
}
}
}
return ret;
}
/** Get the sorted constructor list for the given class. */
public static List getConstructors(JCClass cls) {
TreeSet ts = new TreeSet();
JCConstructor[] constructors = cls.getConstructors();
for (int i = constructors.length - 1; i >= 0; i--) {
ts.add(constructors[i]);
}
return new ArrayList(ts);
}
/** Get all the interfaces the class/interface
* implements/extends.
*/
public static List getAllInterfaces(JCClass cls) {
ArrayList ret = new ArrayList();
collectInterfaces(cls, ret);
return ret;
}
/** Accumulate the subinterfaces recursively */
private static void collectInterfaces(JCClass cls, ArrayList clsList) {
JCClass[] ifcs = cls.getInterfaces();
if (ifcs != null) {
JCFinder finder = JCompletion.getFinder();
for (int i = 0; i < ifcs.length; i++) {
clsList.add(ifcs[i]);
cls = finder.getExactClass(ifcs[i].getFullName());
if (cls != null && clsList.indexOf(cls) < 0) {
collectInterfaces(cls, clsList); // recurse implemented interfaces
}
}
}
}
/** Get the list containing the given class and all its superclasses. */
public static List getSuperclasses(JCClass cls) {
ArrayList clsList = new ArrayList();
JCFinder finder = JCompletion.getFinder();
cls = finder.getExactClass(cls.getFullName());
if (cls != null) {
cls = cls.getSuperclass();
}
while (cls != null && clsList.indexOf(cls) < 0) {
clsList.add(cls);
cls = finder.getExactClass(cls.getFullName());
if (cls != null) {
cls = cls.getSuperclass();
}
}
return clsList;
}
public static boolean isAssignable(JCType from, JCType to) {
JCClass fromCls = from.getClazz();
JCClass toCls = to.getClazz();
if (fromCls.equals(JCompletion.NULL_CLASS)) {
return to.getArrayDepth() > 0 || !JCompletion.isPrimitiveClass(toCls);
}
if (toCls.equals(JCompletion.OBJECT_CLASS)) { // everything is object
return (from.getArrayDepth() > to.getArrayDepth())
|| (from.getArrayDepth() == to.getArrayDepth()
&& !JCompletion.isPrimitiveClass(fromCls));
}
if (from.getArrayDepth() != to.getArrayDepth()) {
return false;
}
if (fromCls.equals(toCls)) {
return true; // equal classes
}
if (fromCls.isInterface()) {
return toCls.isInterface()
&& (getAllInterfaces(fromCls).indexOf(toCls) >= 0);
} else { // fromCls is a class
int fromClsKwdInd = JavaKeywords.getKeyword(fromCls.getName());
if (fromClsKwdInd >= 0) { // primitive class
int toClsKwdInd = JavaKeywords.getKeyword(toCls.getName());
return toClsKwdInd >= 0 && primitivesAssignable[fromClsKwdInd][toClsKwdInd];
} else {
if (toCls.isInterface()) {
return (getAllInterfaces(fromCls).indexOf(toCls) >= 0);
} else { // toCls is a class
return (getSuperclasses(fromCls).indexOf(toCls) >= 0);
}
}
}
}
public static JCType getCommonType(JCType typ1, JCType typ2) {
if (typ1.equals(typ2)) {
return typ1;
}
int cls1KwdInd = JavaKeywords.getKeyword(typ1.getClazz().getName());
int cls2KwdInd = JavaKeywords.getKeyword(typ2.getClazz().getName());
if (cls1KwdInd < 0 && cls2KwdInd < 0) { // non-primitive classes
if (isAssignable(typ1, typ2)) {
return typ1;
} else if (isAssignable(typ2, typ1)) {
return typ2;
} else {
return null;
}
} else { // at least one primitive class
if (typ1.getArrayDepth() != typ2.getArrayDepth()) {
return null;
}
if (cls1KwdInd >= 0 && cls2KwdInd >= 0) {
return JCompletion.getType(primitivesCommonClass[cls1KwdInd][cls2KwdInd],
typ1.getArrayDepth());
} else { // one primitive but other not
return null;
}
}
}
public static JCClass createSimpleClass(Class c) {
if (c == null || c.getName() == null) {
return JCompletion.INVALID_CLASS;
}
return createSimpleClassImpl(c.getName());
}
private static JCClass createSimpleClassImpl(String className) {
int dotInd = className.lastIndexOf('.');
return JCompletion.getSimpleClass(className.replace('$', '.'),
(dotInd >= 0) ? dotInd : 0);
}
public static JCompletion.BaseType createType(Class c) {
if (c == null) {
return JCompletion.INVALID_TYPE;
}
String className = c.getName();
int arrayDepth = 0;
while (className.length() > 0 && className.charAt(0) == '[') {
arrayDepth++;
className = className.substring(1);
}
if (arrayDepth > 0) {
switch (className.charAt(0)) {
case 'L':
className = className.substring(1, className.length() - 1);
break;
case 'B':
className = "byte"; // NOI18N
break;
case 'C':
className = "char"; // NOI18N
break;
case 'D':
className = "double"; // NOI18N
break;
case 'F':
className = "float"; // NOI18N
break;
case 'I':
className = "int"; // NOI18N
break;
case 'J':
className = "long"; // NOI18N
break;
case 'S':
className = "short"; // NOI18N
break;
case 'Z':
className = "boolean"; // NOI18N
break;
}
}
return new JCompletion.BaseType(
createSimpleClassImpl(className),
arrayDepth
);
}
public static List getClassList(List classNames,
boolean storeDeclaredClasses,
int classLevel, int fieldLevel, int methodLevel) {
ArrayList l = new ArrayList();
Iterator i = classNames.iterator();
while (i.hasNext()) {
String name = (String)i.next();
Class c = null;
try {
c = Class.forName(name);
} catch (ClassNotFoundException e) {
System.err.println("Class '" + name + "' not found."); // NOI18N
} catch (Throwable t) {
System.err.println("Exception thrown during class rebuild:"); // NOI18N
t.printStackTrace();
}
if (c != null) {
l.addAll(createClassList(c, storeDeclaredClasses,
classLevel, fieldLevel, methodLevel));
}
}
return l;
}
private static String strip(String name, String baseName, String suffix) {
int startInd = 0;
int endStrip = 0;
if (name.startsWith(baseName)) {
startInd = baseName.length();
}
if (name.endsWith(suffix)) {
endStrip = suffix.length();
}
return name.substring(startInd, name.length() - endStrip);
}
private static String separatorToDot(String s) {
return s.replace(File.separatorChar, '.');
}
private static List createClassList(Class c, boolean storeDeclaredClasses,
int classLevel, int fieldLevel, int methodLevel) {
ArrayList cL = new ArrayList();
if (c == null) {
return cL;
}
if (JCompletion.getLevel(c.getModifiers()) >= classLevel) {
cL.add(new BaseJCClass(c, classLevel, fieldLevel, methodLevel));
}
// possibly store declared classes subclasses
if (storeDeclaredClasses) {
try {
Class[] dC = c.getDeclaredClasses();
for (int i = 0; i < dC.length; i++) {
if (JCompletion.getLevel(dC[i].getModifiers()) >= classLevel) {
cL.addAll(createClassList(dC[i], storeDeclaredClasses,
classLevel, fieldLevel, methodLevel));
}
}
} catch (SecurityException e) {
// can't access declared classes
e.printStackTrace();
}
}
return cL;
}
public static List getClassNameList(String packageDirName) {
File packageDir = new File(packageDirName);
List classNames = new ArrayList();
packageDirName += File.separator; // to strip begining
if (packageDir.exists()) {
getClassListFromSourcesRec(classNames, packageDirName, packageDir);
}
return classNames;
}
private static void getClassListFromSourcesRec(final List l,
final String packageDirName, File curDir) {
curDir.listFiles(
new FileFilter() {
public boolean accept(File f) {
if (f.isDirectory()) {
getClassListFromSourcesRec(l, packageDirName, f);
}
if (f.getName().endsWith(".java")) { // NOI18N
l.add(separatorToDot(strip(f.getAbsolutePath(), packageDirName, ".java"))); // NOI18N
}
return false;
}
}
);
}
public static class BaseJCClass extends JCompletion.AbstractClass {
Class c;
int classLevel;
int fieldLevel;
int methodLevel;
/** Do reflection of given class */
public BaseJCClass(Class c, int classLevel, int fieldLevel,
int methodLevel) {
this.c = c;
this.classLevel = classLevel;
this.fieldLevel = fieldLevel;
this.methodLevel = methodLevel;
JCClass sc = createSimpleClass(c);
name = sc.getName();
packageName = sc.getPackageName();
modifiers = c.getModifiers();
if (c.isInterface()) {
modifiers |= JCompletion.INTERFACE_BIT;
}
}
protected void init() {
body = new Body();
ArrayList lst = new ArrayList();
body.superClass = createSimpleClass(c.getSuperclass());
// create interface classes
Class[] dI = c.getInterfaces();
for (int i = 0; i < dI.length; i++) {
if (JCompletion.getLevel(dI[i].getModifiers()) >= classLevel) {
lst.add(createSimpleClass(dI[i]));
}
}
body.interfaces = new JCClass[lst.size()];
lst.toArray(body.interfaces);
lst.clear();
// create fields
try {
Field[] dF = c.getDeclaredFields();
for (int i = 0; i < dF.length; i++) {
if (JCompletion.getLevel(dF[i].getModifiers()) >= fieldLevel) {
lst.add(new JCompletion.BaseField(this, dF[i].getName(),
createType(dF[i].getType()), dF[i].getModifiers()));
}
}
body.fields = new JCField[lst.size()];
lst.toArray(body.fields);
lst.clear();
} catch (SecurityException e) {
// can't access declared fields
e.printStackTrace();
}
// create constructors
try {
Constructor[] dC = c.getDeclaredConstructors();
for (int i = 0; i < dC.length; i++) {
if (JCompletion.getLevel(dC[i].getModifiers()) >= methodLevel) {
// get constructor parameters
JCParameter[] parameters = JCompletion.EMPTY_PARAMETERS;
try {
Class[] dP = dC[i].getParameterTypes();
parameters = new JCParameter[dP.length];
for (int j = 0; j < dP.length; j++) {
parameters[j] = new JCompletion.BaseParameter(
"", // name not known from reflection // NOI18N
createType(dP[j]));
}
} catch (SecurityException e) {
// can't get parameter types
e.printStackTrace();
}
// get thrown exceptions - don't restrict to classes level
JCClass[] exceptions = JCompletion.EMPTY_CLASSES;
try {
Class[] dE = dC[i].getExceptionTypes();
exceptions = new JCClass[dE.length];
for (int j = 0; j < dE.length; j++) {
exceptions[j] = createSimpleClass(dE[j]);
}
} catch (SecurityException e) {
// can't get exception types
e.printStackTrace();
}
lst.add(new JCompletion.BaseConstructor(this, dC[i].getModifiers(),
parameters, exceptions));
}
}
body.constructors = new JCConstructor[lst.size()];
lst.toArray(body.constructors);
lst.clear();
} catch (SecurityException e) {
// can't access declared constructors
e.printStackTrace();
}
// create methods
try {
Method[] dM = c.getDeclaredMethods();
for (int i = 0; i < dM.length; i++) {
if (JCompletion.getLevel(dM[i].getModifiers()) >= methodLevel) {
// get method parameters
JCParameter[] parameters = JCompletion.EMPTY_PARAMETERS;
try {
Class[] dP = dM[i].getParameterTypes();
parameters = new JCParameter[dP.length];
for (int j = 0; j < dP.length; j++) {
parameters[j] = new JCompletion.BaseParameter(
"", // name not known from reflection // NOI18N
createType(dP[j]));
}
} catch (SecurityException e) {
// can't get parameter types
e.printStackTrace();
}
// get thrown exceptions - don't restrict to classes level
JCClass[] exceptions = JCompletion.EMPTY_CLASSES;
try {
Class[] dE = dM[i].getExceptionTypes();
exceptions = new JCClass[dE.length];
for (int j = 0; j < dE.length; j++) {
exceptions[j] = createSimpleClass(dE[j]);
}
} catch (SecurityException e) {
// can't get exception types
e.printStackTrace();
}
lst.add(new JCompletion.BaseMethod(this,
dM[i].getName(), dM[i].getModifiers(),
createType(dM[i].getReturnType()),
parameters, exceptions));
}
}
body.methods = new JCMethod[lst.size()];
lst.toArray(body.methods);
lst.clear();
} catch (SecurityException e) {
// can't access declared methods
e.printStackTrace();
}
c = null; // can free this reference now
}
}
}
/*
* Log
* 15 Gandalf 1.14 1/13/00 Miloslav Metelka Localization
* 14 Gandalf 1.13 12/28/99 Miloslav Metelka
* 13 Gandalf 1.12 11/24/99 Miloslav Metelka
* 12 Gandalf 1.11 11/14/99 Miloslav Metelka
* 11 Gandalf 1.10 11/11/99 Miloslav Metelka Removed debug print
* 10 Gandalf 1.9 11/9/99 Miloslav Metelka
* 9 Gandalf 1.8 11/8/99 Miloslav Metelka
* 8 Gandalf 1.7 10/23/99 Ian Formanek NO SEMANTIC CHANGE - Sun
* Microsystems Copyright in File Comment
* 7 Gandalf 1.6 9/15/99 Miloslav Metelka
* 6 Gandalf 1.5 7/22/99 Miloslav Metelka
* 5 Gandalf 1.4 7/21/99 Miloslav Metelka
* 4 Gandalf 1.3 7/20/99 Miloslav Metelka
* 3 Gandalf 1.2 6/11/99 Miloslav Metelka
* 2 Gandalf 1.1 6/10/99 Miloslav Metelka
* 1 Gandalf 1.0 6/8/99 Miloslav Metelka
* $
*/